﻿// Copyright (c) Microsoft Corporation.  All rights reserved. 
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using Microsoft.WindowsMobile.Samples.Location;     // Reference to Windows Mobile Sample for GPS
using WiE.Model;                                    // The data model for the application
using WiE.Utils;                                    // Utilities that help with settings and checking network connectivity

namespace WiE
{
    /// <summary>
    /// Main form for the application.  Handles user events and notifications from the GPS module, interacts with the Model to store and retrieve data objects.
    /// </summary>
    public partial class MainForm : Form
    {
        private const string APPLICATION_NAME = "WiE Community Mobile Client";

        // Setting names
        // private const string SETTING_MEMBERID = "settingMemberID";
        private const string SETTING_USERNAME = "settingUserName";
        private const string SETTING_PASSWORD = "settingMemberPassword";
        private const string SETTING_DEVICEID = "settingDeviceID";
        private const string SETTING_AUTOSIGNIN = "settingAutoSignIn";
        private const string SETTING_DATAGOVERNOR_MINIMUM_ELLAPSED_TIME = "settingMinEllapsedTime";
        private const string SETTING_DATAGOVERNOR_MAXIMUM_ELLAPSED_TIME = "settingMaxEllapsedTime";
        private const string SETTING_DATAGOVERNOR_MINIMUM_DISTANCE = "settingMinimumDistance";

        // Default setting values
        private const bool DEFAULT_AUTOSIGNIN = true;
        private const double DEFAULT_DATAGOVERNOR_MINIMUM_DISTANCE = 10.0; // 10 Meters
        private const double DEFAULT_DATAGOVERNOR_MAXIMUM_ELLAPSED_TIME = 60.0; // 60 seconds
        private const double DEFAULT_DATAGOVERNOR_MINIMUM_ELLAPSED_TIME = 5.0; // 5 seconds;

        // Application Setting Values (retrieved and set by LoadSettings method)
        private System.Guid m_guidDeviceID = System.Guid.Empty;
        private string m_strMemberUserName = null;
        private string m_strMemberPassword = null;
        private bool m_bAutoSignIn = DEFAULT_AUTOSIGNIN;
        private double m_nMinimumDistanceBetweenLocationInMeters = DEFAULT_DATAGOVERNOR_MINIMUM_DISTANCE;
        private double m_nMaximumEllapsedTimeBetweenLocationInSeconds = DEFAULT_DATAGOVERNOR_MAXIMUM_ELLAPSED_TIME;
        private double m_nMinimumEllapsedTimeBetweenLocationInSeconds = DEFAULT_DATAGOVERNOR_MINIMUM_ELLAPSED_TIME;

        private DateTime m_dtApplicationStartTime = DateTime.UtcNow;

        // Handler for display updates in the display thread.
        private EventHandler m_updateDisplayHandler = null;

        // Gps Module and Gps related state
        private Gps m_gps = new Gps();
        private GpsPosition m_currentPosition = null;
        private GpsDeviceState m_currentState = null;

        // Most recently reported and stored location
        private WiELocationDataObject m_previousLocation = null;
    
        // The data model for the application
        private IWiEModel m_model = null;

        // Current member 
        private WiEMemberDataObject m_currentMember = null;

        // Current device
        private WiEDeviceDataObject m_currentDevice = null;

        /// <summary>
        /// Constructor for the main form
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            // Try to create the (data) model for the application
            m_model = new WiEModelWithLocalCache();
        }

        /// <summary>
        /// Attempts to retrieve the device data object for this device or creates one if one doesn't 
        /// currently exist.
        /// </summary>
        /// <returns></returns>
        private bool RetrieveAndInitializeDevice()
        {
            bool bSuccess = true;

            // Attempt to retrieve the device object for the device, or initialize a new
            // device object if this is the first time the application has been installed
            // and run on this device.
            WiEDeviceDataObject deviceDataObject = m_model.GetDevice(m_guidDeviceID);
            if (null == deviceDataObject)
            {
                // Generate a unique GUID for the device
                if (m_guidDeviceID == System.Guid.Empty)
                    m_guidDeviceID = System.Guid.NewGuid();

                // Create a new device object for the device
                deviceDataObject = new WiEDeviceDataObject();

                deviceDataObject.DeviceID = m_guidDeviceID;

                // Save the generated device ID
                SaveSettings();
            }

            m_currentDevice = deviceDataObject;

            return bSuccess;
        }

        /// <summary>
        /// Attempts to retrieve the member information for the current user of the device and validates
        /// the password if "auto-sign-in" preference was selected.
        /// </summary>
        /// <returns></returns>
        private bool RetrieveMemberAndAutoSignIn()
        {
            bool bSignedIn = false;

            if (null != m_strMemberUserName)
            {
                // Attempt to retrieve the member information from the data model.
                if (null != (m_currentMember = m_model.GetMemberByUserName(m_strMemberUserName)))
                {
                    // Did the user choose to autologin?
                    if (m_bAutoSignIn)
                    {
                        // If user has selected to always login automatically, validate the user's 
                        // password against the member object's password to make sure they still 
                        // match.  If they don't match, the member may have changed his password on the
                        // server so we need to prompt him again to enter it.
                        if (false == (bSignedIn = (m_currentMember.Password == m_strMemberPassword)))
                        {
                            // The password did not match, so we'll need to prompt the user to login manually
                            // rather than autologin.
                            MessageBox.Show("Could not automatically sign in.  Please verify your username and password.", "Unable to login", MessageBoxButtons.OK, MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
                        }
                    }
                }
            }

            // return if we successfully retrieved the member and validated his or her password
            return bSignedIn;
        }

        /// <summary>
        /// Attempts to retrieve the settings & saved values for the application
        /// </summary>
        private void LoadSettings()
        {            
            ApplicationSettings applicationSettings = new ApplicationSettings(APPLICATION_NAME);

            // Retrieve the device ID or generate a new one if none is defined (first time run)
            string strDeviceID = applicationSettings.GetDeviceSetting(SETTING_DEVICEID);
            if (null != strDeviceID)
                m_guidDeviceID = new System.Guid(strDeviceID);

            // Retrieve the member user name if we saved it.
            m_strMemberUserName = applicationSettings.GetUserSetting(SETTING_USERNAME);

            // Retrieve saved user password
            m_strMemberPassword = applicationSettings.GetUserSetting(SETTING_PASSWORD);

            // Retrieve the user's preference for autologin
            string strAutoSignIn = applicationSettings.GetUserSetting(SETTING_AUTOSIGNIN);
            if (null != strAutoSignIn)
                m_bAutoSignIn = bool.Parse(strAutoSignIn);
        }

        /// <summary>
        /// Saves the applications current settings, including the MemberID and DeviceID for this instance
        /// of the 
        /// </summary>
        private void SaveSettings()
        {
            ApplicationSettings applicationSettings = new ApplicationSettings(APPLICATION_NAME);

            try
            {
                applicationSettings.SaveDeviceSetting(SETTING_DEVICEID, m_guidDeviceID.ToString());
                applicationSettings.SaveUserSetting(SETTING_USERNAME, m_strMemberUserName);
                applicationSettings.SaveUserSetting(SETTING_PASSWORD, m_strMemberPassword);
                applicationSettings.SaveUserSetting(SETTING_AUTOSIGNIN, m_bAutoSignIn.ToString());
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("There was an issue saving one or more of the settings for the application. Exception:" + ex.ToString());
            }
        }

        /// <summary>
        /// Prompts the user for his or her login information or to register
        /// </summary>
        /// <returns></returns>
        private bool PromptUserForSignupOrLogin()
        {
            bool bUserValidated = false;

            // Create a new login / join form
            FormJoinCommunity formJoinCommunity = new FormJoinCommunity();

            // Populate the form if we are dealing with an existing user, most likely someone that
            // chose not to "login automatically".
            if (null != m_currentMember)
            {
                // Prepopulate the member's user name as the member is most likely
                // updating his or her password.
                formJoinCommunity.UserName = m_currentMember.UserName;
                formJoinCommunity.FirstName = m_currentMember.FirstName;
                formJoinCommunity.LastName = m_currentMember.LastName;
                formJoinCommunity.Password = m_currentMember.Password;
                formJoinCommunity.Email = m_currentMember.Email;
                formJoinCommunity.PhoneNumber = m_currentMember.PhoneNumber;
                formJoinCommunity.AutoSignIn = m_bAutoSignIn;
                formJoinCommunity.ExistingMember = true;
            }

            // Show the dialog box and wait for the user to press OK
            if (formJoinCommunity.ShowDialog() == DialogResult.OK)
            {
                // Did user select that he or she was an existing member to the 
                // community?
                if (formJoinCommunity.ExistingMember)
                {
                    // This is an existing member, just retrieve his or her information 
                    // and validate the password...
                    WiEMemberDataObject memberDataObject = m_model.GetMemberByUserName(formJoinCommunity.UserName);
                    if (null == memberDataObject)
                    {
                        MessageBox.Show("Unable to validate your username, please try again.", "Unknown UserName", MessageBoxButtons.OK, MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
                    }
                    else if (memberDataObject.Password != formJoinCommunity.Password)
                    {
                        MessageBox.Show("Invalid password, please try again.", "Invalid Password", MessageBoxButtons.OK, MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
                    }
                    else
                    {
                        // Remember what the user has selected.
                        m_bAutoSignIn = formJoinCommunity.AutoSignIn;
                        m_currentMember = memberDataObject;
                        bUserValidated = true;
                    }
                }
                else
                {
                    // This is a new member, we need to create a new member out of the information we
                    // collected using the form and then attempt to save the member information.
                    m_currentMember = new WiEMemberDataObject();
                    m_currentMember.MemberID = System.Guid.NewGuid();
                    m_currentMember.FirstName = formJoinCommunity.FirstName;
                    m_currentMember.LastName = formJoinCommunity.LastName;
                    m_currentMember.Email = formJoinCommunity.Email;
                    m_currentMember.PhoneNumber = formJoinCommunity.PhoneNumber;
                    m_currentMember.UserName = formJoinCommunity.UserName;
                    m_currentMember.Password = formJoinCommunity.Password;

                    try
                    {
                        // Now save the member
                        m_model.SaveMember(m_currentMember);
                        bUserValidated = true;                        
                    }
                    catch (Exception saveMemberException)
                    {
                        System.Diagnostics.Trace.WriteLine("There was a problem saving the registration: " + saveMemberException.ToString());
                        MessageBox.Show("There was a problem saving the registration.", "Problem", MessageBoxButtons.OK, MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
                    }
                }
            }

            if (bUserValidated)
            {
                // Remember the member ID, Password, etc. when saving settings.
                m_strMemberUserName = m_currentMember.UserName;
                m_strMemberPassword = m_currentMember.Password;
                m_bAutoSignIn = formJoinCommunity.AutoSignIn;

                // Update the device status to reflect the current member
                UpdateDeviceStatus();

                // Let's remember the username, password and autologin preference based on
                // what the user entered.
                SaveSettings();
            }

            return (bUserValidated);
        }

        /// <summary>
        /// User has selected to exit the Where Is Ed? application.  Make sure to clean up any open GPS related events and possibly take
        /// one last shot at synchronizing back to the 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnExitRequested(object sender, EventArgs e)
        {
            // Save any settings we want updated.
            SaveSettings();

            // Close the connection to the GPS module
            if (m_gps.Opened)
            {
                m_gps.Close();
            }

            // Tell the model to clean itself up
            if (m_model != null)
            {
                m_model.Terminate();
                m_model = null;
            }

            // Close the main form and the application
            Close();
        }

        /// <summary>
        /// User has requested that GPS tracking be initiated
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnStartGPSRequested(object sender, EventArgs e)
        {
            // Open connection to the GPS device if it is not already open
            if (!m_gps.Opened)
            {
                m_gps.Open();
            }

            // Update the UI to reflect the current state
            this.menuItemStartGPS.Enabled = false;
            this.menuItemStopGPS.Enabled = true;
        }

        /// <summary>
        /// User has requested that GPS tracking be turned off
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnStopGPSRequested(object sender, EventArgs e)
        {
            // Close the GPS connection if we have it open
            if (m_gps.Opened)
            {
                m_gps.Close();
            }

            // Update the UI to reflect the new state...
            this.menuItemStopGPS.Enabled = false;
            this.menuItemStartGPS.Enabled = true;
        }


        /// <summary>
        /// Event callback when form is first loaded.  We initialize the GPS event handlers here and the event handler for 
        /// redrawing / updating the UI.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnFormLoad(object sender, EventArgs e)
        {
            // Try to load previously saved settings if any or create default
            // values if first time loaded.
            LoadSettings();

            // Initialize the model as we will be using it to retrieve data from local and 
            // remote data stores.
            m_model.Initialize();

            // Ensure that we have assigned a "unique" identifier to this device
            RetrieveAndInitializeDevice();

            // Attempt to log the user into the application, or if this is a first run
            // then prompt the user for login information
            if (false == RetrieveMemberAndAutoSignIn())
            {
                // If this is the first time being used, or if the user selected not to automatically
                // login we prompt the user for his or her login or ask them to join.
                if (false == PromptUserForSignupOrLogin())
                {
                    // User cancelled the login or registration, so we want to exit.
                    Close();

                    // Don't bother doing anything else.
                    return;
                }
            }

            if (m_currentMember != null)
                m_labelMemberName.Text = m_currentMember.FirstName + " " + m_currentMember.LastName;

            // Set up display handler that will be used to update the display 
            // on the display thread.
            m_updateDisplayHandler += new EventHandler(UpdateDisplayRequest);

            // Hook the GPS module's device status events to our event handler
            m_gps.DeviceStateChanged += new DeviceStateChangedEventHandler(OnDeviceStateChanged);

            // Hook the GPS module's location notifications to our event handler 
            m_gps.LocationChanged += new LocationChangedEventHandler(OnLocationChanged);

        }

        /// <summary>
        /// Event callback when a new location is received from the GPS module.
        /// </summary>
        /// <revision>
        /// 10-28-2008 O.S.M  - Added check for DOP to throw out bad readings.
        /// </revision>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        void OnLocationChanged(object sender, LocationChangedEventArgs args)
        {
            // Retrieve the GPS position information from the args
            m_currentPosition = args.Position;

            try
            {
                // Only do this if we got GPS data.
                if ((m_gps.Opened) && 
                    (m_currentPosition != null))
                {
                    // We need at least the longitude and the latitude and good DOP (<=6) for this to be worth saving...
                    if ((m_currentPosition.LatitudeValid && m_currentPosition.LongitudeValid && m_currentPosition.TimeValid && m_currentPosition.SatellitesInViewCountValid && m_currentPosition.PositionDilutionOfPrecisionValid) && (m_currentPosition.PositionDilutionOfPrecision <= 6))
                    {
                        // Ok, now populate the new Location Data Object
                        WiELocationDataObject newLocation = new WiELocationDataObject();

                        // Set up the base required fields (Long, Lat, Time, SatellitesInView)
                        newLocation.LocationID = System.Guid.NewGuid();
                        newLocation.MemberID = m_currentMember.MemberID;
                        newLocation.DeviceID = m_currentDevice.DeviceID;
                        newLocation.Longitude = m_currentPosition.Longitude;
                        newLocation.Latitude = m_currentPosition.Latitude;
                        newLocation.DateCollected = m_currentPosition.Time;
                        newLocation.NumSatellites = m_currentPosition.SatellitesInViewCount;

                        // Now set the optional fields
                        if (m_currentPosition.SpeedValid)
                            newLocation.Speed = m_currentPosition.Speed;

                        if (m_currentPosition.HeadingValid)
                            newLocation.Heading = m_currentPosition.Heading;

                        if (m_currentPosition.SeaLevelAltitudeValid)
                            newLocation.AltitudeWRTSeaLevel = m_currentPosition.SeaLevelAltitude;

                        if (m_currentPosition.EllipsoidAltitudeValid)
                            newLocation.AltitudeWRTEllipsoid = m_currentPosition.EllipsoidAltitude;

                        // Now avoid saving too much data by applying a data capture governot that only saves 
                        // every [m_nMinimumDistanceBetweenLocationInMeters] meters and at least every [m_nMaximumEllapsedTimeBetweenLocationInSeconds]
                        // seconds but not more frequently than [m_nMinimumEllapsedTimeBetweenLocationInSeconds].
                        double dDistanceFromPreviousPoint = (null == m_previousLocation) ? m_nMinimumDistanceBetweenLocationInMeters : newLocation.DistanceTo(m_previousLocation);
                        double dSecondsEllapsedSincePreviousPoint = (null == m_previousLocation) ? m_nMaximumEllapsedTimeBetweenLocationInSeconds : ((TimeSpan)newLocation.DateCollected.Value.Subtract(m_previousLocation.DateCollected.Value)).TotalSeconds;

                        if  ((dSecondsEllapsedSincePreviousPoint>=m_nMinimumEllapsedTimeBetweenLocationInSeconds)&&
                            ((dDistanceFromPreviousPoint >= m_nMinimumDistanceBetweenLocationInMeters) ||
                             (dSecondsEllapsedSincePreviousPoint >= m_nMaximumEllapsedTimeBetweenLocationInSeconds)))
                        {
                            // Attempt to save the the location
                            m_model.SaveLocation(newLocation);

                            // Remember the location for next time around in case we want to implement a simple
                            // governor to minimize amount of data collected.
                            m_previousLocation = newLocation;

                            // Update the status associated with the device to reflect this latest position.
                            UpdateDeviceStatus();
                        }
                    }

                    // Update the display using the display thread by using the Invoke() method
                    // and show the current location
                    Invoke(m_updateDisplayHandler);
                }
            }
            catch (Exception ex)
            {
                // We ran into an issue with the GPS module.
                System.Diagnostics.Trace.WriteLine("Issue in the GPS event handler: " + ex.ToString());
            }

        }

        /// <summary>
        /// Updates the device data object with the current status of the device (last known location and 
        /// client "heartbeat" time.
        /// </summary>
        private void UpdateDeviceStatus()
        {
            // Update the device's last known location
            if (null != m_previousLocation)
            {
                m_currentDevice.LastLocationDateCollected = m_previousLocation.DateCollected;
                m_currentDevice.LastLocationLatitude = m_previousLocation.Latitude;
                m_currentDevice.LastLocationLongitude = m_previousLocation.Longitude;
            }

            // Make sure we have the start time for the device
            m_currentDevice.ClientStarted = m_dtApplicationStartTime;

            // Update the heartbeat time.
            m_currentDevice.ClientHeartbeat = DateTime.UtcNow;

            // Update the member ID associated with the device to reflect the current
            // member.
            m_currentDevice.MemberID = (m_currentMember != null) ? m_currentMember.MemberID : System.Guid.Empty;

            try
            {
                // Attempt to save the device object.
                m_model.SaveDevice(m_currentDevice);
            }
            catch (Exception updateDeviceException)
            {
                System.Diagnostics.Trace.WriteLine("Issue updating the device status and last known position: " + updateDeviceException.ToString());
            }
        }

        /// <summary>
        /// Event callback when we receive a change notification from GPS about a change to the device
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        void OnDeviceStateChanged(object sender, DeviceStateChangedEventArgs args)
        {
            m_currentState = args.DeviceState;

            // Update the display to show the current status
            Invoke(m_updateDisplayHandler);
        }

        /// <summary>
        /// Update the display to show the current status.
        /// </summary>
        protected void UpdateDisplayRequest(object sender, System.EventArgs args)
        {
            // Check to make sure we have GPS connection
            try
            {
                if (m_gps.Opened)
                {
                    string str = "";

                    if (m_currentState != null)
                    {
                        str = m_currentState.FriendlyName + " " + m_currentState.ServiceState + ", " + m_currentState.DeviceState + "\n";
                    }

                    if (m_currentPosition != null)
                    {
                        if (m_previousLocation != null)
                        {
                            str += "Last Save: " + m_previousLocation.DateCollected.ToString() + "\n\n";
                        }

                        // 10-28-2008 - Added output of DOP to see quality of reading.
                        if (m_currentPosition.PositionDilutionOfPrecisionValid)
                        {
                            str += "Dilution (DOP):\n" + m_currentPosition.PositionDilutionOfPrecision + "\n";
                        }

                        if (m_currentPosition.LatitudeValid)
                        {
                            str += "Latitude (DD):\n   " + m_currentPosition.Latitude + "\n";
                        }

                        if (m_currentPosition.LongitudeValid)
                        {
                            str += "Longitude (DD):\n   " + m_currentPosition.Longitude + "\n";
                        }

                        if (m_currentPosition.SatellitesInSolutionValid &&
                            m_currentPosition.SatellitesInViewValid &&
                            null != m_currentPosition.GetSatellitesInView() &&
                            m_currentPosition.SatelliteCountValid)
                        {
                            str += "Satellite Count:\n   " + m_currentPosition.GetSatellitesInSolution().Length + "/" +
                                m_currentPosition.GetSatellitesInView().Length + " (" +
                                m_currentPosition.SatelliteCount + ")\n";
                        }

                        if (m_currentPosition.TimeValid)
                        {
                            str += "Time:\n   " + m_currentPosition.Time.ToString() + "\n";
                        }

                        if (m_currentPosition.SpeedValid)
                        {
                            str += "Speed:\n " + m_currentPosition.Speed + "\n";
                        }

                        if (m_currentPosition.HeadingValid)
                        {
                            str += "Heading:\n " + m_currentPosition.Heading + "\n";
                        }

                        if (m_currentPosition.SeaLevelAltitudeValid)
                        {
                            str +="Altitude:\n " + m_currentPosition.SeaLevelAltitude + "\n";
                        }
                    }

                    this.m_labelMain.Text = str;
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("Problem updating display: " + ex.ToString());

                this.m_labelMain.Text = ex.ToString();
            }
        }
    }
}